#include <stdio.h>
#include <string.h>

#include <algorithm>
#include <vector>

#include <SDL.h>
#include <SDL_net.h>
#include <SDL_mixer.h>
#include <cryptlib.h>
#include <sha.h>

#include "StringEncoding.h"
#include "Config.h"
#include "FileSystem.h"
#include "EncryptedStream.h"

const int windowWidth = 640;
const int windowHeight = 360;

SDL_Window *sdlWindow = NULL;
SDL_Renderer *sdlRenderer = NULL;
SDL_Event sdlEvent = {};
bool sdlQuit = false;

Mix_Music *mixMusic = NULL;

const int musicMaxSamples = 1024;

Sint16 musicBuffer[musicMaxSamples * 2] = {};
SDL_Point musicPoints[musicMaxSamples] = {};
int musicBufferNumSamples = 0;

class FileSender {
private:
	std::wstring root;
	std::vector<FindFiles::FileData> files;
	std::vector<std::wstring> directories;
	unsigned long long totalSize;

	void enumerateInternal(const std::wstring& path, bool recursive) {
		FindFiles ff(path);

		printf("%s ::\n", StringEncoding::fromUTF16(path, StringEncoding::ANSI).c_str());
		for (const auto& f : ff.files) {
			totalSize += f.fileSize;

			FindFiles::FileData f2 = f;
			f2.fileName = path + f.fileName;
			files.push_back(f2);

			printf("%I64d %s\n", f.fileSize, StringEncoding::fromUTF16(f.fileName, StringEncoding::ANSI).c_str());
		}

		if (recursive) {
			for (const auto& f : ff.directories) {
				directories.push_back(path + f.fileName);
				enumerateInternal(path + f.fileName, recursive);
			}
		}
	}

public:

	FileSender() : totalSize(0) {
	}

	void enumerate(std::wstring path, bool recursive) {
		root.clear();
		files.clear();
		directories.clear();
		totalSize = 0;

		if (path.empty()) return;

		if (path.find_last_of(L"\\/") != path.size() - 1) path += L"\\";

		root = path;

		enumerateInternal(path, recursive);

		printf("Total size: %I64d\n", totalSize);
	}

	void send(EncryptedStream& stream) {
		unsigned long long bytesSent = 0;
		std::vector<char> buf(1048576);

		// first send all directories
		for (const auto& fd: directories) {
			if(!stream.sendString(std::string("mkdir"), 1)) return;
			if(!stream.sendString(fd.substr(root.size()), 2)) return;
		}

		Uint32 startTime = SDL_GetTicks();

		// then send all files
		for (int n = 0, m = files.size(); n < m; n++) {
			const auto& fd = files[n];

			FILE *f = _wfopen(fd.fileName.c_str(), L"rb");
			if (f == NULL) {
				printf("[ERROR] Failed to open file '%s'\n", StringEncoding::fromUTF16(fd.fileName, StringEncoding::ANSI).c_str());
				bytesSent += fd.fileSize;
			} else {
				if (!stream.sendString(std::string("send"), 1)
					|| !stream.sendString(fd.fileName.substr(root.size()), 2)
					|| stream.send(&fd.fileSize, sizeof(fd.fileSize)) != sizeof(fd.fileSize)) {
					fclose(f);
					return;
				}

				CryptoPP::SHA256 hash;
				unsigned long long remaining = fd.fileSize;
				while (remaining > 0) {
					size_t i = (size_t)std::min<unsigned long long>(remaining, buf.size());
					size_t j = fread(buf.data(), 1, i, f);
					if (j <= 0) {
						printf("[ERROR] Unexpected end of file\n");
						fclose(f);
						stream.destroy();
						return;
					}

					hash.Update((const CryptoPP::byte*)buf.data(), j);
					if (stream.send(buf.data(), j) != j) {
						fclose(f);
						return;
					}

					bytesSent += j;
					remaining -= j;

					Uint32 t = SDL_GetTicks();
					float perc = 0.0f;
					int hh = 99, mm = 99, ss = 99;
					int speed = 0;
					if (totalSize > 0) perc = float((double)bytesSent / (double)totalSize * 100.0);
					if (bytesSent > 0) {
						int dt = t - startTime;
						ss = int((float)dt / 1000.0f / perc * (100.0f - perc));
						hh = ss / 3600;
						mm = (ss / 60) % 60;
						ss = ss % 60;
						if (dt > 0) speed = int((double)bytesSent / (double)dt * 1000.0 / 1024.0 + 0.5);
					}
					printf("F: %d/%d B: %I64d/%I64d %dKB/s %0.2f%% ETA %02d:%02d:%02d \r", n + 1, m, bytesSent, totalSize, speed, perc, hh, mm, ss);
				}

				fclose(f);

				hash.Final((CryptoPP::byte*)buf.data());
				if (stream.send(buf.data(), hash.DigestSize()) != hash.DigestSize()) {
					return;
				}
			}
		}
	}
};

static void SDLCALL fakeSoundEffect(void *udata, Uint8 *stream, int len) {
	// NOTE: len is in bytes, not in samples !!!
	len = std::max(std::min(len / 4, musicMaxSamples), 0);
	memcpy(musicBuffer, stream, len * 4);
	musicBufferNumSamples = len;
}

int main(int argc, char** argv) {
	Config::loadConfig();

	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | SDL_INIT_TIMER) == -1) {
		printf("ERROR: failed to initalize SDL2\n");
		return -1;
	}

	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1) {
		printf("ERROR: failed to initalize SDL2_mixer\n");
		SDL_Quit();
		return -1;
	}

	if (SDLNet_Init() == -1) {
		printf("ERROR: failed to initalize SDL2_net\n");
		Mix_CloseAudio();
		Mix_Quit();
		SDL_Quit();
		return -1;
	}

	if (SDL_CreateWindowAndRenderer(windowWidth, windowHeight, 0, &sdlWindow, &sdlRenderer) == -1) {
		printf("ERROR: failed to create window\n");
		Mix_CloseAudio();
		Mix_Quit();
		SDLNet_Quit();
		SDL_Quit();
		return -1;
	}

	SDL_SetWindowTitle(sdlWindow, "PotPlayer");

	Mix_SetPostMix(fakeSoundEffect, NULL);

	if (argc >= 2) {
		mixMusic = Mix_LoadMUS(argv[1]);
		if (mixMusic == NULL) {
			std::string s = StringEncoding::convert(argv[1], StringEncoding::UTF8, StringEncoding::ANSI);
			printf("ERROR: failed to load music '%s'\n", s.c_str());
		} else {
			std::string s = "PotPlayer - " + std::string(argv[1]);
			SDL_SetWindowTitle(sdlWindow, s.c_str());
			Mix_PlayMusic(mixMusic, -1);
		}
	}

	printf("Press Ctrl+C to connect to server\n"
		"Press Ctrl+E to enumerate files\n"
		"Press Ctrl+S to send files\n"
		"Press Ctrl+Q to quit\n");

	EncryptedStream stream;
	FileSender fileSender;

	while (true) {
		while (SDL_PollEvent(&sdlEvent)) {
			switch (sdlEvent.type) {
			case SDL_QUIT:
				sdlQuit = true;
				break;
			case SDL_KEYDOWN:
				if (sdlEvent.key.keysym.mod & KMOD_CTRL) {
					switch (sdlEvent.key.keysym.sym) {
					case SDLK_q:
						sdlQuit = true;
						break;
					case SDLK_c:
						if (stream.sock == NULL) {
							IPaddress ipaddr;
							std::string host = Config::getConfig("host");
							if (SDLNet_ResolveHost(&ipaddr, host.c_str(), Config::getConfigAsInt("port")) < 0) {
								printf("ERROR: failed to resolve host '%s'\n", host.c_str());
							} else {
								TCPsocket sock = SDLNet_TCP_Open(&ipaddr);
								if (sock == NULL) {
									printf("ERROR: failed to connect to host '%s'\n", host.c_str());
								} else {
									printf("Connected to host '%s'\n", host.c_str());
									stream.create(sock);
								}
							}
						}
						break;
					case SDLK_e:
						fileSender.enumerate(StringEncoding::toUTF16(Config::getConfig("root"), StringEncoding::ANSI),
							Config::getConfigAsInt("recursive") != 0);
						break;
					case SDLK_s:
						if (stream.sock != NULL) {
							fileSender.send(stream);
						}
					}
				}
				break;
			}
		}
		
		stream.dropIncoming();

		if (sdlQuit) break;

		SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, 255);
		SDL_RenderClear(sdlRenderer);

		if (musicBufferNumSamples >= 2) {
			SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BLENDMODE_ADD);

			for (int i = 0; i < musicBufferNumSamples; i++) {
				musicPoints[i] = SDL_Point{
					(int)((float)windowWidth * (float)i / (float)musicBufferNumSamples + 0.5f),
					(int)((float)windowHeight * (0.5f - (float)musicBuffer[i * 2] / (float)65536.0f) + 0.5f),
				};
			}
			SDL_SetRenderDrawColor(sdlRenderer, 255, 0, 255, 255);
			SDL_RenderDrawLines(sdlRenderer, musicPoints, musicBufferNumSamples);

			for (int i = 0; i < musicBufferNumSamples; i++) {
				musicPoints[i] = SDL_Point{
					(int)((float)windowWidth * (float)i / (float)musicBufferNumSamples + 0.5f),
					(int)((float)windowHeight * (0.5f - (float)musicBuffer[i * 2 + 1] / (float)65536.0f) + 0.5f),
				};
			}
			SDL_SetRenderDrawColor(sdlRenderer, 0, 255, 0, 255);
			SDL_RenderDrawLines(sdlRenderer, musicPoints, musicBufferNumSamples);

			SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BLENDMODE_BLEND);
		}

		SDL_RenderPresent(sdlRenderer);
		SDL_Delay(30);
	}

	stream.destroy();
	SDLNet_Quit();

	Mix_SetPostMix(NULL, NULL);
	Mix_HaltMusic();
	if (mixMusic) {
		Mix_FreeMusic(mixMusic);
		mixMusic = NULL;
	}
	Mix_CloseAudio();
	Mix_Quit();

	SDL_DestroyRenderer(sdlRenderer);
	SDL_DestroyWindow(sdlWindow);
	SDL_Quit();

	return 0;
}
